﻿//  -------------------- iSharePoint -------------------- //
// 
//  Company:	IT Complex, LLC
//  Project:	Smart.Common
//  File name:	SPH.cs
//  Developer:	Solomatov Igor
//  Created:	15.04.2012
// 
//  -------------------- iSharePoint -------------------- //

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;

namespace Smart.Common
{
	[Localizable(false)]
	[DebuggerStepThrough]
	public static class SPH
	{
		/// <summary>
		/// Minimal supported date in SharePoint UI
		/// </summary>
		public static DateTime MinDate = new DateTime(1900, 1, 1);

		public static DateTime CvtToDT(this object value, bool InISO8601)
		{
			if (value == null) return DateTime.MinValue;
			if (value is DateTime) return (DateTime)value;
			if (InISO8601 && value is string)
				return SPUtility.CreateDateTimeFromISO8601DateTimeString((string)value);

			return Convert.ToDateTime(value);
		}

		public static void DispatchLookupValue(string value, out int id, out string name)
		{
			id = -1;
			name = string.Empty;

			if (string.IsNullOrEmpty(value) || value.Length <= 2) return;

			string sId;

			DispatchDelimitValue(value, out sId, out name);

			if (!int.TryParse(sId, out id))
			{
				id = -1;
				name = string.Empty;
			}

			if (id != 0)
				return;

			id = -1;
			name = string.Empty;
		}

		public const string LookupValueDelimiter = ";#";
		public const string MultiLookupValueDelimiter = ";#";

		public static void DispatchDelimitValue(string value, out string prePart, out string postPart)
		{
			value.DispatchDelimitValue(LookupValueDelimiter, out prePart, out postPart);
		}

		public static string FormatLookupValue(int id, string title)
		{
			return String.Concat(id, LookupValueDelimiter, title);
		}

		public static object GetFormulaValue(string itemValue)
		{
			string valueType;
			string value;

			SPH.DispatchDelimitValue(itemValue, out valueType, out value);

			if (H.IsAnyNullOrEmpty(valueType, value))
				return null;

			switch (valueType.ToUpper())
			{
				case "ERROR":
					return null;

				case "FLOAT":
					return Convert.ToDouble(value.Replace(",", "."), H.En);

				case "DATETIME":
					return SPUtility.CreateSystemDateTimeFromXmlDataDateTimeFormat(value);

				case "BOOLEAN":
					return value.In("true", "-1", "1");

				default:
					return value.IsNullOrEmpty() ? null : value;
			}
		}

		public static SPListItemCollection GetFilteredItems(SPList list, string query, uint rowLimit, Dictionary<string, object> properties)
		{
			if (list == null || rowLimit <= 0) return null;

#warning Using ViewFields + ViewFieldsOnly will significantly improve performance!

			var cq =
				new SPQuery
					{
						Query = query,
						RowLimit = rowLimit,
						ItemIdQuery = rowLimit == 1,
						ViewAttributes = "Scope=\"Recursive\""
					};

			if (properties != null)
			{
				if (properties.ContainsKey("ExpandRecurrence"))
					cq.ExpandRecurrence = properties.SafeGetBool("ExpandRecurrence", false);

				if (properties.ContainsKey("CalendarDate"))
					cq.CalendarDate = properties.SafeGetDT("CalendarDate", DateTime.Now);
			}

			H.LT("Query list: {0}, {1}", list.Title, query.IsNullOrEmpty() ? "<empty>" : query);

			return list.GetItems(cq);
		}

		#region Get Current XXXX methods

		/// <summary>
		/// Returns current (original request) site from SPContext or DefaultCtx
		/// </summary>
		public static SPSite GetCurrentSite()
		{
			var ctx = GetCtx();

			if (ctx == null)
			{
				var d = GetDefaultCtx();

				return
					d != null
						? d.Site
						: null;
			}

			return ctx.Site;
		}

		/// <summary>
		/// Returns current (original request) web from SPContext or DefaultCtx
		/// </summary>
		public static SPWeb GetCurrentWeb()
		{
			var ctx = GetCtx();

			if (ctx == null)
			{
				var d = GetDefaultCtx();

				return
					d != null
						? d.Web
						: null;
			}

			return ctx.Web;
		}

		/// <summary>
		/// Returns current (original request) list from SPContext or DefaultCtx
		/// </summary>
		public static SPList GetCurrentList()
		{
			var ctx = GetCtx();

			if (ctx == null)
			{
				var d = GetDefaultCtx();

				return
					d != null
						? d.List
						: null;
			}

			return ctx.List;
		}

		/// <summary>
		/// Returns current (original request) item from SPContext or DefaultCtx
		/// </summary>
		public static SPListItem GetCurrentItem()
		{
			var ctx = GetCtx();

			if (ctx == null)
			{
				var d = GetDefaultCtx();

				return
					d != null
						? d.Item
						: null;
			}

			return ctx.ListItem;
		}

		/// <summary>
		/// Returns current (original request) user from SPContext or DefaultCtx
		/// </summary>
		public static SPUser GetCurrentUser()
		{
			var ctx = GetCtx();

			SPWeb web = null;

			if (ctx == null)
			{
				var d = GetDefaultCtx();

				if (d != null && d.User != null)
					return d.User;

				web = GetCurrentWeb();
			}
			else
			{
				web = ctx.Web;
			}

			return web == null ? null : web.CurrentUser;
		}

		/// <summary>
		/// Returns current (original request) LCID from SPContext or DefaultCtx
		/// </summary>
		public static int GetCurrentLCID()
		{
			var user = GetCurrentUser();

			if (user != null && user.RegionalSettings != null && user.RegionalSettings.ServerLanguage != null)
				return user.RegionalSettings.ServerLanguage.LCID;

			var web = GetCurrentWeb();

			if (web != null)
				return (int)web.Language;

			return 0;
		}

		/// <summary>
		/// Returns current (original request) url from SPContext or DefaultCtx
		/// </summary>
		public static string GetCurrentUrl()
		{
			var ctx = HttpContext.Current;

			return
				ctx != null
					? ctx.Request.RawUrl
					: GetCurrentWebUrl();
		}

		/// <summary>
		/// Returns current (original request) Web Url from SPContext or DefaultCtx
		/// </summary>
		public static string GetCurrentWebUrl()
		{
			var web = GetCurrentWeb();

			return
				web != null
					? web.Url
					: null;
		}

		/// <summary>
		/// Checks whether current user is in group by <paramref name="groupName"/>
		/// </summary>
		public static bool IsCurrentUserInGroup(string groupName)
		{
			return GetCurrentUser().InGroup(groupName);
		}

		/// <summary>
		/// Checks whether current user is in group by <paramref name="groupId"/>
		/// </summary>
		public static bool IsCurrentUserInGroup(int groupId)
		{
			return GetCurrentUser().InGroup(groupId);
		}

		/// <summary>
		/// Checks whether <paramref name="principalId"/> is current user himself or is current user in group by <paramref name="principalId"/>
		/// </summary>
		public static bool IsCurrentUserOrHisGroup(int principalId)
		{
			return GetCurrentUser().IsHimselfOrHisGroup(principalId);
		}

		/// <summary>
		/// Checks whether <paramref name="loginOrName"/> is current user himself or is current user in group by <paramref name="loginOrName"/>
		/// </summary>
		public static bool IsCurrentUserOrHisGroup(string loginOrName)
		{
			return GetCurrentUser().IsHimselfOrHisGroup(loginOrName);
		}

		#endregion

		#region Process Values, Prepare Values

		public static bool ProcessFieldValues(SPListItem source, SPListItem destination, string[] sourceFieldTitles, string[] destinationFieldTitles, H.ProcessMethod method)
		{
			var res = true;

			if (source == null || destination == null || sourceFieldTitles == null || destinationFieldTitles == null || sourceFieldTitles.Length != destinationFieldTitles.Length ||
				sourceFieldTitles.Length <= 0 || destinationFieldTitles.Length <= 0) return false;

			for (var I = 0; I < sourceFieldTitles.Length; I++)
			{
				var sf = sourceFieldTitles[I];
				var df = destinationFieldTitles[I];

				try
				{
					if (method == H.ProcessMethod.Copy)
						destination[df] = source[sf];
					else
						destination[df] = H.ProcessValue(source[sf], destination[df], method);
				}
				catch (Exception x)
				{
					res = false;

					H.LX("ProcessFieldValues1", x);
				}
			}

			return res;
		}

		public static bool ProcessFieldValues(Dictionary<string, object> source, SPListItem destination, string[] sourceFieldTitles, string[] destinationFieldTitles, H.ProcessMethod method)
		{
			var res = true;

			if (source == null || destination == null || sourceFieldTitles == null || destinationFieldTitles == null || sourceFieldTitles.Length != destinationFieldTitles.Length ||
				sourceFieldTitles.Length <= 0 || destinationFieldTitles.Length <= 0) return false;

			for (var I = 0; I < sourceFieldTitles.Length; I++)
			{
				var sf = sourceFieldTitles[I];
				var df = destinationFieldTitles[I];

				try
				{
					if (method == H.ProcessMethod.Copy)
						destination[df] = source[sf];
					else
						destination[df] = H.ProcessValue(source[sf], destination[df], method);
				}
				catch (Exception x)
				{
					res = false;

					H.LX("ProcessFieldValues2", x);
				}
			}

			return res;
		}

		public static Dictionary<string, object> PrepareValues(SPListItem item, SPItemEventDataCollection values, List<string> fields)
		{
			return PrepareValues(item, values, fields.ToArray());
		}

		public static Dictionary<string, object> PrepareValues(SPListItem item, SPItemEventDataCollection values, params string[] fields)
		{
			return PrepareValues(item, values, false, fields);
		}

		public static Dictionary<string, object> PrepareValues(SPListItem item, SPItemEventDataCollection values, bool detectDifferences, params string[] fields)
		{
			var res = new Dictionary<string, object>();

			if (item != null)
			{
				var list = item.ParentList;

				for (var I = 0; I < fields.Length; I++)
				{
					var field = fields[I];

					var iValue = (item[field]);
					var vValue = (values != null ? values[list.GetFieldName(field)] : null);

					var fDef = list.GetField(field);

					if (fDef.Type == SPFieldType.Lookup && values != null)
					{
						var flDef = (SPFieldLookup)fDef;

						var lValueId = vValue.SToInt(-1);

						var lValueTitle =
							lValueId > 0
								? list.ParentWeb.GetListByName(flDef.LookupList)
									  .GetItemValue(lValueId, flDef.LookupField)
									  .SToStr(null)
								: string.Empty;

						vValue = (lValueId > 0 ? FormatLookupValue(lValueId, lValueTitle) : null);
					}

					if (!detectDifferences || H.CompareValue(iValue, vValue) != 0)
					{
						if (vValue != null)
							res[field] = vValue;
						else
							res[field] = iValue;
					}
				}
			}

			return res;
		}

		#endregion

		#region Ctx
		public static SPContext GetCtx()
		{
			return SPContext.Current;
		}
		#endregion

		#region DefaultCtx

		[ThreadStatic]
		public static DefaultCtxStack DefaultCtx;

		/// <summary>
		/// Returns current default context
		/// </summary>
		public static ElevationCtxInfo GetDefaultCtx()
		{
			return DefaultCtx.PeekCtx();
		}

		/// <summary>
		/// Set context (for Timer Jobs and so on)
		/// </summary>
		public static CtxElevator<ElevationCtxInfo> SetContext(SPSite site, SPWeb web)
		{
			return DefaultCtx.NewCtx(new ElevationCtxInfo { Site = site, Web = web, InElevatedMode = false });
		}

		/// <summary>
		/// Set context (for Event receiver)
		/// </summary>
		public static CtxElevator<ElevationCtxInfo> SetContext(SPSite site, SPWeb web, SPList list, SPListItem item)
		{
			return DefaultCtx.NewCtx(new ElevationCtxInfo { Site = site, Web = web, List = list, Item = item, InElevatedMode = false });
		}

		/// <summary>
		/// Set context (for Workflow)
		/// </summary>
		public static CtxElevator<ElevationCtxInfo> SetContext(SPSite site, SPWeb web, SPList list, SPListItem item, SPUser user)
		{
			return DefaultCtx.NewCtx(new ElevationCtxInfo { Site = site, Web = web, List = list, Item = item, User = user, InElevatedMode = false });
		}

		#endregion

		#region Elevated Ctx

		[ThreadStatic]
		public static ElevationCtxStack ElevationCtx;

		/// <summary>
		/// Returns current elevation context
		/// </summary>
		public static ElevationCtxInfo GetElevationCtx()
		{
			return ElevationCtx.PeekCtx();
		}

		public static Stack<ElevationCtxInfo> GetElevationStack()
		{
			return ElevationCtx.Stack;
		}

		public static void PushElevationCtx(ElevationCtxInfo elevationCtxInfo)
		{
			ElevationCtx.PushCtx(elevationCtxInfo);
		}

		public static ElevationCtxInfo PopElevationCtx()
		{
			return ElevationCtx.PopCtx();
		}

		public static ElevationCtxInfo PeekElevationCtx()
		{
			return ElevationCtx.PeekCtx();
		}

		public static bool RunUnder(string siteUrl, SPSecurity.CodeToRunElevated code)
		{
			var res = true;

			if (UnderElevatedMode())
			{
				H.LT("RunUnder: Reusing elevated mode");

				EnterUnderMode(siteUrl);

				try
				{
					code();
				}
				catch (Exception x)
				{
					res = false;
					H.LX("RunUnder.r", x);
					throw new TargetInvocationException(x);
				}
				finally
				{
					LeaveElevatedMode();
				}
			}
			else
			{
				H.LT("RunUnder: Using new elevated mode");

				ValidateFormDigest();

				SPSecurity.RunWithElevatedPrivileges(
					delegate
					{
						EnterUnderMode(siteUrl);

						try
						{
							code();
						}
						catch (Exception x)
						{
							res = false;
							H.LX("RunUnder.n", x);
							throw new TargetInvocationException(x);
						}
						finally
						{
							LeaveElevatedMode();
						}
					});
			}

			return res;
		}

		public static bool RunElevated(SPSecurity.CodeToRunElevated code)
		{
			var res = true;

			if (UnderElevatedMode())
			{
				// Reuse elevated mode, instead of entering it twice

				H.LT("RunElevated: Reusing elevated mode");

				EnterElevatedMode();

				try
				{
					code();
				}
				catch (Exception x)
				{
					res = false;
					H.LX("RunElevated.r", x);
					throw new TargetInvocationException(x);
				}
				finally
				{
					LeaveElevatedMode();
				}
			}
			else
			{
				H.LT("RunElevated: Using new elevated mode");

				SPH.ValidateFormDigest();

				SPSecurity.RunWithElevatedPrivileges(
					delegate
					{
						EnterElevatedMode();

						try
						{
							code();
						}
						catch (Exception x)
						{
							res = false;
							H.LX("RunElevated.n", x);
							throw new TargetInvocationException(x);
						}
						finally
						{
							LeaveElevatedMode();
						}
					});
			}

			return res;
		}

		public static bool UnderElevatedMode()
		{
			var ctxInfo = PeekElevationCtx();

			var inElevatedMode = (ctxInfo != null && ctxInfo.InElevatedMode);

			return inElevatedMode;
		}

		public static void EnterUnderMode(string siteUrl)
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			H.LT("*** EnterUnderMode L{0}, {1} ***", (inElevatedMode ? el.CtxIndex + 1 : 0), inElevatedMode);

			var site = new SPSite(siteUrl);
			var web = site.OpenWeb();

			H.LT("*** Using new SPSite and new SPWeb ***");

			var nel =
				new ElevationCtxInfo
					{
						InElevatedMode = true,
						Site = site,
						SiteDispose = true,
						Web = web,
						WebDispose = true
					};

			PushElevationCtx(nel);
		}

		public static void EnterElevatedMode()
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			H.LT("*** EnterElevatedMode L{0}, {1} ***", (inElevatedMode ? el.CtxIndex + 1 : 0), inElevatedMode);

			ElevationCtxInfo nel;

			if (inElevatedMode)
			{
				H.LT("*** Reusing SPSite and SPWeb ***");

				nel =
					new ElevationCtxInfo
						{
							InElevatedMode = true,
							Site = el.Site,
							SiteDispose = false,
							Web = el.Web,
							WebDispose = false
						};
			}
			else
			{
				var siteId = Guid.Empty;
				var siteZone = SPUrlZone.Default;
				var webId = Guid.Empty;

				var spCtx = SPContext.Current;

				if (spCtx != null)
				{
					siteId = spCtx.Site.ID;
					siteZone = spCtx.Site.Zone;
					webId = spCtx.Web.ID;
				}
				else
				{
					var ctx = GetDefaultCtx();

					if (ctx.Site != null)
					{
						siteId = ctx.Site.ID;
						siteZone = ctx.Site.Zone;
					}

					if (ctx.Web != null)
						webId = ctx.Web.ID;
				}

				H.LT("*** Using new SPSite and new SPWeb ***");

				var site = new SPSite(siteId, siteZone);
				var web = site.OpenWeb(webId);

				nel =
					new ElevationCtxInfo
						{
							InElevatedMode = true,
							Site = site,
							SiteDispose = true,
							Web = web,
							WebDispose = true
						};
			}

			nel.SiteOriginalAUU = nel.Site.AllowUnsafeUpdates;
			nel.WebOriginalAUU = nel.Web.AllowUnsafeUpdates;

			PushElevationCtx(nel);
		}

		public static void EnterElevatedMode(SPWeb web)
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			H.LT("*** EnterElevatedModeW L{0}, {1} ***", (inElevatedMode ? el.CtxIndex + 1 : 0), inElevatedMode);

			ElevationCtxInfo nel = null;

			if (inElevatedMode)
			{

				var wid = web.ID;
				var sid = web.Site.ID;
				var szone = web.Site.Zone;

				SPSite eSite = null;

				var info =
					SearchElevationStack(
						delegate(ElevationCtxInfo ctxInfo)
						{
							if (wid == ctxInfo.Web.ID && szone == ctxInfo.Site.Zone)
								return true;

							if (sid == ctxInfo.Site.ID && szone == ctxInfo.Site.Zone)
								eSite = ctxInfo.Site;

							return false;
						});

				if (info != null)
				{
					H.LT("*** Reusing SPSite and SPWeb ***");

					nel =
						new ElevationCtxInfo
							{
								InElevatedMode = true,
								Site = info.Site,
								SiteDispose = false,
								Web = info.Web,
								WebDispose = false
							};
				}
				else if (eSite != null)
				{
					H.LT("*** Reusing SPSite only and using new SPWeb ***");

					nel =
						new ElevationCtxInfo
							{
								InElevatedMode = true,
								Site = eSite,
								SiteDispose = false,
								Web = eSite.OpenWeb(web.ID),
								WebDispose = true
							};
				}
			}

			if (nel == null)
			{
				var site = web.Site;
				var siteId = site.ID;
				var siteZone = site.Zone;
				var webId = web.ID;

				H.LT("*** Using new SPSite and new SPWeb ***");

				var nsite = new SPSite(siteId, siteZone);
				var nweb = nsite.OpenWeb(webId);

				nel =
					new ElevationCtxInfo
						{
							InElevatedMode = true,
							Site = nsite,
							SiteDispose = true,
							Web = nweb,
							WebDispose = true
						};
			}

			nel.SiteOriginalAUU = nel.Site.AllowUnsafeUpdates;
			nel.WebOriginalAUU = nel.Web.AllowUnsafeUpdates;

			PushElevationCtx(nel);
		}

		public static void EnterElevatedMode(SPSite site)
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			H.LT("*** EnterElevatedModeS L{0}, {1} ***", (inElevatedMode ? el.CtxIndex + 1 : 0), inElevatedMode);

			ElevationCtxInfo nel = null;

			if (inElevatedMode)
			{
				var sid = site.ID;
				var szone = site.Zone;

				var info = SearchElevationStack(ctxInfo => sid == ctxInfo.Site.ID && szone == ctxInfo.Site.Zone);

				if (info != null)
				{
					H.LT("*** Reusing SPSite only and using new SPWeb ***");

					nel =
						new ElevationCtxInfo
							{
								InElevatedMode = true,
								Site = info.Site,
								SiteDispose = false,
								Web = info.Site.OpenWeb(),
								WebDispose = true
							};
				}
			}

			if (nel == null)
			{
				var siteId = site.ID;
				var siteZone = site.Zone;

				H.LT("*** Using new SPSite and new SPWeb ***");

				var nsite = new SPSite(siteId, siteZone);
				var nweb = nsite.OpenWeb();

				nel =
					new ElevationCtxInfo
						{
							InElevatedMode = true,
							Site = nsite,
							SiteDispose = true,
							Web = nweb,
							WebDispose = true
						};
			}

			nel.SiteOriginalAUU = nel.Site.AllowUnsafeUpdates;
			nel.WebOriginalAUU = nel.Web.AllowUnsafeUpdates;

			PushElevationCtx(nel);
		}

		public static void LeaveElevatedMode()
		{
			var el = PopElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			if (inElevatedMode)
			{
				// it is bad to forgot to reset it, but this will help me to stay secured (but this way is not recomended)

				if (el.Site.AllowUnsafeUpdates != el.SiteOriginalAUU)
				{
					el.Site.AllowUnsafeUpdates = el.SiteOriginalAUU;
					H.LT("ElSite.Exception: UnsafeUpdate was modified (restored to {0})!", el.SiteOriginalAUU);
				}

				if (el.Web.AllowUnsafeUpdates != el.WebOriginalAUU)
				{
					el.Web.AllowUnsafeUpdates = el.WebOriginalAUU;
					H.LT("ElWeb.Exception: UnsafeUpdate was modified (restored to {0})!", el.WebOriginalAUU);
				}

				if (el.Web != null && el.WebDispose)
					el.Web.Dispose();

				el.Web = null;

				if (el.Site != null && el.SiteDispose)
					el.Site.Dispose();

				el.Site = null;

				el.InElevatedMode = false;
			}

			H.LT("*** LeaveElevatedMode L{0}, {1} ***", (inElevatedMode ? el.CtxIndex : 0), UnderElevatedMode());
		}

		#endregion

		#region Get/Find Sites/Webs

		/// <summary>
		/// Returns current elevated site
		/// </summary>
		public static SPSite GetElevatedSite()
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			return inElevatedMode ? el.Site : null;
		}

		/// <summary>
		/// Returns current elevated web
		/// </summary>
		public static SPWeb GetElevatedWeb()
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			return inElevatedMode ? el.Web : null;
		}

		/// <summary>
		/// Returns elevated web by <paramref name="id"/>
		/// </summary>
		public static ManagedDisposable<SPWeb> GetElevatedWeb(Guid id)
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			if (!inElevatedMode)
				return null;

			var szone = el.Site.Zone;

			var info = SearchElevationStack(ctxInfo => ctxInfo.Web.ID == id && szone == ctxInfo.Site.Zone);

			if (info != null)
			{
				H.LT("*** Reusing SPWeb ***");

				return new ManagedDisposable<SPWeb>(info.Web, false);
			}

			if (el.Site.RootWeb.ID == id)
			{
				H.LT("*** Reusing SPWeb ***");

				return new ManagedDisposable<SPWeb>(el.Site.RootWeb, false);
			}

			H.LT("*** Using new SPWeb ***");

			return new ManagedDisposable<SPWeb>(el.Site.OpenWeb(id), true);
		}

		/// <summary>
		/// Returns current context web
		/// </summary>
		public static SPWeb GetWeb()
		{
			var el = PeekElevationCtx();

			return el != null ? el.Web : GetCurrentWeb();
		}

		/// <summary>
		/// Searches in elevation stack for reusing
		/// </summary>
		private static ElevationCtxInfo SearchElevationStack(Func<ElevationCtxInfo, bool> check)
		{
			var stack = GetElevationStack();

			if (stack == null) return null;

			// ReSharper disable LoopCanBeConvertedToQuery
			foreach (var info in stack)
				if (info != null && check(info))
					return info;
			// ReSharper restore LoopCanBeConvertedToQuery

			return null;
		}

		/// <summary>
		/// Returns Web by id. Return value should be disposed!
		/// </summary>
		public static ManagedDisposable<SPWeb> GetWeb(Guid id)
		{
			var site = GetSite();

			if (UnderElevatedMode())
			{
				var zone = site.Zone;
				var info = SearchElevationStack(ctxInfo => id == ctxInfo.Web.ID && zone == ctxInfo.Site.Zone);

				if (info != null)
				{
					H.LT("*** Reusing SPWeb ***");

					return new ManagedDisposable<SPWeb>(info.Web, false);
				}
			}
			else
			{
				var web = GetWeb();

				if (web != null && web.ID == id)
				{
					H.LT("*** Reusing SPWeb ***");

					return new ManagedDisposable<SPWeb>(web, false);
				}

				if (site != null && site.RootWeb.ID == id)
				{
					H.LT("*** Reusing SPWeb ***");

					return new ManagedDisposable<SPWeb>(site.RootWeb, false);
				}
			}

			H.LT("*** Using new SPWeb ***");

			return
				site != null
					? new ManagedDisposable<SPWeb>(site.OpenWeb(id), true)
					: null;
		}

		/// <summary>
		/// Returns Web by Url. Return value should be disposed!
		/// </summary>
		public static ManagedDisposable<SPWeb> GetWeb(string url, bool requireExactUrl)
		{
			SPSite site = null;
			string fullUrl = null;

			var urlType = url.DetectUrlType();

			switch (urlType)
			{
				case UrlType.None:
					return null;

				case UrlType.Abolute:
					fullUrl = url;
					break;

				case UrlType.ServerRelative:
				case UrlType.SiteRelative:
					site = GetSite();
					fullUrl = site.MakeFullUrl(url);
					break;

				default:
					return null;
			}

			var info =
				SearchElevationStack(
					delegate(ElevationCtxInfo ctxInfo)
					{
						if (ctxInfo.Web.Url.EqIC(fullUrl))
							return true;

						if (fullUrl.StartsWith(ctxInfo.Site.Url, StringComparison.OrdinalIgnoreCase))
							site = ctxInfo.Site;

						return false;
					});

			if (info != null)
			{
				H.LT("*** Reusing SPWeb ***");

				return new ManagedDisposable<SPWeb>(info.Web, false);
			}

			var web = GetWeb();

			if (web != null && web.Url.EqIC(fullUrl))
			{
				H.LT("*** Reusing SPWeb ***");

				return new ManagedDisposable<SPWeb>(web, false);
			}

			if (site == null)
				site = GetSite();

			web = site.RootWeb;

			if (web != null && web.Url.EqIC(fullUrl))
			{
				H.LT("*** Reusing SPWeb ***");

				return new ManagedDisposable<SPWeb>(web, false);
			}

			H.LT("*** Using new SPWeb ***");

			return
				site != null
					? new ManagedDisposable<SPWeb>(site.OpenWeb(fullUrl, requireExactUrl), true)
					: null;
		}

		/// <summary>
		/// Finds Web by Url. Return value should be disposed!
		/// </summary>
		public static ManagedDisposable<SPWeb> FindWebByUrl(string url, bool trimListsOrLib)
		{
			var fixedUrl = url != "/" ? url.TrimEnd('/') : url;

			if (trimListsOrLib)
			{
				var listsIdx = fixedUrl.IndexOf("/Lists/", StringComparison.OrdinalIgnoreCase);

				if (listsIdx > 0)
				{
					fixedUrl = fixedUrl.Substring(0, listsIdx);
				}
				else
				{
					var firstIdx = fixedUrl.IndexOf('/');
					var lastIdx = firstIdx > 0 ? fixedUrl.LastIndexOf('/') : -1;

					if (firstIdx + 1 < lastIdx)
						fixedUrl = fixedUrl.Substring(0, lastIdx);
				}
			}

			return GetWeb(fixedUrl, false);
		}

		/// <summary>
		/// Returns current context site
		/// </summary>
		public static SPSite GetSite()
		{
			var el = PeekElevationCtx();

			var inElevatedMode = (el != null && el.InElevatedMode);

			if (inElevatedMode)
			{
				//el.elWebRefCnt++;

				return el.Site;
			}
			else
			{
				var ctx = GetCtx();

				if (ctx == null)
				{
					var dctx = GetDefaultCtx();

					return
						dctx != null ? dctx.Site : null;
				}

				var site = ctx.Site;

				return site;
			}
		}

		/// <summary>
		/// Returns current context site's RootWeb
		/// </summary>
		public static SPWeb GetRootWeb()
		{
			var site = GetSite();

			return site == null ? null : site.RootWeb;
		}

		/// <summary>
		/// Finds Web, using <paramref name="webPath"/> (real server-relative url with "*" placeholders)
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="webPath"/> is null</exception>
		public static SPWeb FindWebMatch(string webPath, out bool shouldBeDisposed)
		{
			if (webPath == null)
				throw new ArgumentNullException("webPath");

			return FindWebMatch(webPath, GetSite(), GetWeb(), out shouldBeDisposed);
		}

		/// <summary>
		/// Finds Web, using <paramref name="webPath"/> (real server-relative url with "*" placeholders)
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="webPath"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="site"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="web"/> is null</exception>
		public static SPWeb FindWebMatch(string webPath, SPSite site, SPWeb web, out bool shouldBeDisposed)
		{
			if (webPath == null) throw new ArgumentNullException("webPath");
			if (site == null) throw new ArgumentNullException("site");
			if (web == null) throw new ArgumentNullException("web");

			var webUrl = FindWebUrlMatch(webPath, web);

			var cwUrl =
				webPath.DetectUrlType() == UrlType.Abolute
					? NormalizeServerRelativeUrl(web.Url)
					: NormalizeServerRelativeUrl(web.ServerRelativeUrl);

			if (webUrl.EqIC(cwUrl))
			{
				shouldBeDisposed = false;

				return web;
			}

			shouldBeDisposed = true;

			return site.AllWebs[webUrl];
		}

		/// <summary>
		/// Finds web Url, using <paramref name="webPath"/> (real server-relative url with "*" placeholders)
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="webPath"/> is null</exception>
		public static string FindWebUrlMatch(string webPath)
		{
			if (webPath == null) throw new ArgumentNullException("webPath");

			return
				webPath.Contains('*')
					? FindWebUrlMatch(webPath, GetWeb())
					: NormalizeServerRelativeUrl(webPath);
		}

		/// <summary>
		/// Finds web Url, using <paramref name="webPath"/> (real server-relative url with "*" placeholders)
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="webPath"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="web"/> is null</exception>
		public static string FindWebUrlMatch(string webPath, SPWeb web)
		{
			if (webPath == null) throw new ArgumentNullException("webPath");
			if (web == null) throw new ArgumentNullException("web");

			var webUrl = NormalizeServerRelativeUrl(webPath);

			if (webUrl.Contains('*'))
			{
				var pathNames = webUrl.Split('/');
				var webNames = NormalizeServerRelativeUrl(web.ServerRelativeUrl).TrimEnd('/').Split('/');

				// Move web by web to fill missed web names, until all available names are found or names start be differencing
				for (var I = 0; I < Math.Min(pathNames.Length, webNames.Length); I++)
				{
					if (pathNames[I].EqIC("*"))
					{
						pathNames[I] = webNames[I];
					}
					else if (!pathNames[I].EqIC(webNames[I]))
					{
						break;
					}
				}

				webUrl = NormalizeServerRelativeUrl("/" + pathNames.CastToString("/"));
			}

			return webUrl;
		}
		#endregion

		#region Urls

		public static void GetImageUrls(string imageUrl, out string thumbUrl, out string wideUrl)
		{
			thumbUrl = null;
			wideUrl = null;

			if (imageUrl.IsNullOrEmpty())
				return;

			var idx = imageUrl.LastIndexOf('/');

			if (idx >= 0)
			{
				thumbUrl = imageUrl.Substring(0, idx) + "/_t" + imageUrl.Substring(idx).Replace('.', '_') + ".jpg";
				wideUrl = imageUrl.Substring(0, idx) + "/_w" + imageUrl.Substring(idx).Replace('.', '_') + ".jpg";
			}
		}

		public static bool IsIgnoredFolder(string folderName)
		{
			return (folderName.In("_t", "_w", "Forms", "_cts"));
		}

		/// <summary>
		/// Normalize server-relative libUrl
		/// </summary>
		public static string NormalizeServerRelativeUrl(string url)
		{
			if (url.IsNullOrEmpty()) return "/";

			return
				url.DetectUrlType() == UrlType.Abolute
					? url.Replace("///", "/").Replace("//", "/").TrimEnd('/')
					: url.Replace("///", "/").Replace("//", "/").EnsureStartsWith("/").TrimEnd('/');
		}

		public static string GetNewFormUrl(string webUrl, Guid listId, string contentTypeId, string rootFolder, bool isDlg)
		{
			return
				contentTypeId.IsNullOrEmpty()
					? string.Concat(webUrl, "/_layouts/listform.aspx?PageType=8&ListId=", listId.ToString("B"),
									"&RootFolder=", rootFolder ?? string.Empty,
									(isDlg ? "&IsDlg=1" : string.Empty))
					: string.Concat(webUrl, "/_layouts/listform.aspx?PageType=8&ListId=", listId.ToString("B"),
									"&ContentTypeID=", contentTypeId,
									"&RootFolder=", rootFolder ?? string.Empty,
									(isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetDocumentSetHomePageUrl(string webUrl, string folderUrl)
		{
			return
				string.Concat(webUrl, "/_layouts/DocSetHome.aspx?id=/", Uri.EscapeUriString(folderUrl.IsNullOrEmpty() ? "null" : folderUrl.TrimStart('/')));
		}

		public static string GetNewDocumentSetPageUrl(string webUrl, Guid listId, string contentTypeId, string rootFolderUrl, bool isDlg)
		{
			return
				string.Concat(webUrl,
							  "/_layouts/NewDocSet.aspx?List=", listId.ToString("B"),
							  "&RootFolder=/", Uri.EscapeUriString(rootFolderUrl.IsNullOrEmpty() ? "null" : rootFolderUrl.TrimStart('/')),
							  "&ContentTypeId=", contentTypeId,
							  (isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetDispFormUrl(string webUrl, Guid listId, int itemId, string contentTypeId, bool isDlg)
		{
			return
				contentTypeId.IsNullOrEmpty()
					? string.Concat(webUrl, "/_layouts/listform.aspx?PageType=4&ListId=", listId.ToString("B"),
									"&ID=", itemId.ToString(H.En),
									(isDlg ? "&IsDlg=1" : string.Empty))
					: string.Concat(webUrl, "/_layouts/listform.aspx?PageType=4&ListId=", listId.ToString("B"),
									"&ID=", itemId.ToString(H.En), "&ContentTypeID=", contentTypeId,
									(isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetUserDispFormUrl(string webUrl, int principalId, bool isDlg)
		{
			return
				string.Concat(webUrl, "/_layouts/userdisp.aspx?ID=", principalId.ToString(H.En), (isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetUserProfileUrl(string webUrl, string login, bool isDlg)
		{
			return
				string.Concat(webUrl, "/Person.aspx?accountname=", login.EncodeUri(), (isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetEditFormUrl(string webUrl, Guid listId, int itemId, string contentTypeId, bool isDlg)
		{
			return
				contentTypeId.IsNullOrEmpty()
					? string.Concat(webUrl, "/_layouts/listform.aspx?PageType=6&ListId=", listId.ToString("B"),
									"&ID=", itemId.ToString(H.En),
									(isDlg ? "&IsDlg=1" : string.Empty))
					: string.Concat(webUrl, "/_layouts/listform.aspx?PageType=6&ListId=", listId.ToString("B"),
									"&ID=", itemId.ToString(H.En), "&ContentTypeID=", contentTypeId,
									(isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetDirectDispFormUrl(MetaWebScope web, Guid listId, int itemId, string contentTypeId, bool isDlg)
		{
			var ml = (MetaWebList)web.List(listId);

			return
				contentTypeId.IsNullOrEmpty()
					? string.Concat( /*web.Url,*/ ml.GetDispFormUrl(contentTypeId), "?ID=", itemId.ToString(H.En),
												  (isDlg ? "&IsDlg=1" : string.Empty))
					: string.Concat( /*web.Url,*/ ml.GetDispFormUrl(contentTypeId), "?ID=", itemId.ToString(H.En),
												  "&ContentTypeID=", contentTypeId,
												  (isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetDirectNewFormUrl(MetaWebScope web, Guid listId, string contentTypeId, string rootFolder, bool isDlg)
		{
			var ml = (MetaWebList)web.List(listId);

			return
				contentTypeId.IsNullOrEmpty()
					? string.Concat( /*web.Url,*/ ml.GetNewFormUrl(contentTypeId),
												  "?RootFolder=", rootFolder ?? string.Empty,
												  (isDlg ? "&IsDlg=1" : string.Empty))
					: string.Concat( /*web.Url,*/ ml.GetNewFormUrl(contentTypeId),
												  "?ContentTypeID=", contentTypeId,
												  "&RootFolder=", rootFolder ?? string.Empty,
												  (isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetDirectEditFormUrl(MetaWebScope web, Guid listId, int itemId, string contentTypeId, bool isDlg)
		{
			var ml = (MetaWebList)web.List(listId);

			return
				contentTypeId.IsNullOrEmpty()
					? string.Concat( /*web.Url,*/ ml.GetEditFormUrl(contentTypeId), "?ID=", itemId.ToString(H.En),
												  (isDlg ? "&IsDlg=1" : string.Empty))
					: string.Concat( /*web.Url,*/ ml.GetEditFormUrl(contentTypeId), "?ID=", itemId.ToString(H.En),
												  "&ContentTypeID=", contentTypeId,
												  (isDlg ? "&IsDlg=1" : string.Empty));
		}

		public static string GetUrlActionSet(string displayName, SPFieldType fieldType, string value, bool hide)
		{
			var ftc = "";

			switch (fieldType)
			{
				case SPFieldType.Counter:
				case SPFieldType.Number:
				case SPFieldType.Integer:
				case SPFieldType.Currency:
				case SPFieldType.Text:
				case SPFieldType.Note:
					ftc = "t";
					break;
				case SPFieldType.DateTime:
					break;
				case SPFieldType.Choice:
					ftc = "c";
					break;
				case SPFieldType.Lookup:
					ftc = "l";
					break;
				case SPFieldType.Boolean:
					ftc = "b";
					break;
				case SPFieldType.User:
					ftc = "p";
					break;
			}

			var qs =
				string.Concat("&dfn=", displayName.EncodeUri(), "&dft=", ftc, "&dfv=", value.EncodeUri());

			if (hide)
				qs += "&dfh=hide";

			return qs;
		}

		#endregion

		#region Config files

		/// <summary>
		/// Read text file from config folder (UTF8 encoding)
		/// </summary>
		public static string LoadConfigFile(string fileName)
		{
			return LoadConfigFile(fileName, Encoding.UTF8);
		}

		/// <summary>
		/// Read file from config folder
		/// </summary>
		public static string LoadConfigFile(string fileName, Encoding encoding)
		{
			try
			{
				var filePath = Path.Combine(SPUtility.GetGenericSetupPath("CONFIG"), fileName);

				return File.ReadAllText(filePath, encoding);
			}
			catch (Exception x)
			{
				H.LX("SPH.LoadTextFileFromConfig", x);
			}

			return null;
		}

		#endregion

		/// <summary>
		/// Validate form digest (smart)
		/// </summary>
		public static bool ValidateFormDigest()
		{
			var page = WebExts.GetPage();

			try
			{
				return
					(page != null) &&
					(page.Request.HttpMethod.EqIC("POST")) &&
					(SPUtility.ValidateFormDigest());
			}
			catch (SPException x)
			{
				H.LT("SPH.ValidateFormDigest: " + x.Message);
			}
			catch (Exception x)
			{
				H.LX("SPH.ValidateFormDigest", x);
			}

			return false;
		}
	}
}